#region Usings

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Castle.ActiveRecord.Queries;
using log4net;
using Karma.Framework.Core.Domain.Tools.Import.Layout;

#endregion

namespace Karma.Framework.Core.Domain.Tools.Import.Validation
{
    public class DefaultReferenceValidator : IReferenceValidator
    {
        private static readonly ILog LOG = LogManager.GetLogger(typeof (DefaultReferenceValidator).Name);

        #region IReferenceValidator Members

        public ValidationSummary Validate<T>(EntityLayout layout, IList<T> entities)
            where T : DomainObject
        {
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug(string.Format("Validating References for entity {0}", layout.Entity.FullName));
            }
            var summary = new ValidationSummary();

            var errors = ValidateEntity(layout, entities);
            if (layout.Aggregates != null)
            {
                foreach (AggregateLayout aggregateLayout in layout.Aggregates)
                {
                    var property = (from p in layout.Entity.GetProperties()
                                    where p.Name.Equals(aggregateLayout.Name)
                                    select p).Single();

                    var aggregates = (from e in entities
                                      select property.GetValue(e, new object[] {})).ToList();

                    var aggErrors = ValidateEntity(aggregateLayout, aggregates);
                    if (aggErrors != null)
                    {
                        errors.Errors = errors.Errors.Concat(aggErrors.Errors).ToList();
                    }
                }
            }

            if (errors != null)
            {
                summary.ErrorSummary = errors;
            }
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug("References validated.");
            }
            return summary;
        }

        #endregion

        private ErrorSummary ValidateEntity<T>(EntityLayout layout, IList<T> entities)
        {
            ErrorSummary summary = null;
            // id
            if (!(layout is AggregateLayout))
            {
                summary = ValidateIdsExists(layout, entities);
            }
            // property reference
            var references = (from p in layout.Properties
                              where p is ReferenceField
                              select p);

            if (LOG.IsDebugEnabled)
            {
                LOG.Debug("References to validate: ");
                foreach (var reference in references)
                {
                    LOG.Debug(string.Format("Reference Name: {0}.", reference.FieldType.Name));
                }
            }
            foreach (ReferenceField reference in references)
            {
                if (LOG.IsDebugEnabled)
                {
                    LOG.Debug(string.Format("Validating references for field [{0}] of type [{1}]", reference.Name, reference.FieldType.Name));
                }
                var property = layout.Entity.GetProperty(reference.Name);
                var refPropName = reference.KeyRef ?? "Id";
                var refProperty = reference.FieldType.GetProperty(refPropName);

                var referenceValues = (from e in entities
                                       let a = property.GetValue(e, new object[] {})
                                       where a != null
                                       select refProperty.GetValue(a, new object[] {}))
                    .Distinct().ToArray();

                #region Query building
                var referencesStr = new StringBuilder();
                var refCount = 0;
                for (var idx = 0; idx < referenceValues.Length; idx++)
                {
                    var refValue = referenceValues[idx];
                    if (refProperty.PropertyType.IsValueType || refProperty.PropertyType.Equals(typeof (DateTime)))
                    {
                        if (!(refProperty.PropertyType == typeof(int) && refValue.Equals(0)))
                        {
                            referencesStr.Append(refValue);
                            refCount++;
                        }
                    }
                    else
                    {
                        referencesStr.Append("'").Append(refValue).Append("'");
                        refCount++;
                    }
                    if (idx < referenceValues.Length - 1)
                    {
                        referencesStr.Append(",");
                    }
                }
                # endregion

                if (refCount > 0)
                {
                    var query = new SimpleQuery<object>(reference.FieldType,
                                                        string.Format(
                                                            "select distinct d.Id, d.{0} from {1} d where d.{0} in ({2})",
                                                            refPropName, reference.FieldType.Name, referencesStr));
                    var result = query.Execute();

                    #region Error reporting
                    if (result.Length != referenceValues.Length)
                    {
                        var ids = (from o in result
                                   select ((object[])o)[reference.KeyRef != null ? 1 : 0]).Cast<object>().ToList();
                        var missings = referenceValues.Cast<object>().Except(ids);
                        var column = layout.Properties.IndexOf(reference);
                        foreach (var missing in missings)
                        {
                            var entitiesWithErrors = (from e in entities
                                                      let p = property.GetValue(e, new object[] { })
                                                      where p == null || refProperty.GetValue(p, new object[] { }).Equals(missing)
                                                      select e);
                            foreach (var e in entitiesWithErrors)
                            {
                                AddErrorToSummary(ref summary, entities.IndexOf(e), column, reference, missing);
                            }
                        }
                    }
                    #endregion
                    else
                    {
                        if (LOG.IsDebugEnabled)
                        {
                            LOG.Debug(string.Format("Updating references for [{0}]: [{1}]",
                                reference.FieldType.Name, referencesStr));
                        }
                        var queryRefs = new SimpleQuery<object>(reference.FieldType,
                                                            string.Format(
                                                                "select d, d.{0} from {1} d where d.{0} in ({2})",
                                                                refPropName, reference.FieldType.Name, referencesStr));
                        var resultRefs = queryRefs.Execute();
                        var setMethod = property.GetSetMethod();
                        foreach (object[] o in resultRefs)
                        {
                            var domain = (DomainObject)o[0];
                            var pVal = o[1];
                            if (LOG.IsDebugEnabled)
                            {
                                LOG.Debug(string.Format("Updating reference [{0}]: Id [{1}], Value [{2}]", reference.FieldType.Name, 
                                    domain.Id, refProperty.GetValue(domain, new object[] { })));
                            }
                            var updated = (from e in entities
                                           let pv = (DomainObject)property.GetValue(e, new object[] { })
                                           where pv != null
                                           let val = refProperty.GetValue(pv, new object[] { })
                                           let isstr = val is string
                                           let flag = isstr? ((string)val).ToLower().Equals(((string)pVal).ToLower()) :
                                                refProperty.GetValue(pv, new object[] { }).Equals(pVal)
                                           where flag
                                           let a = setMethod.Invoke(e, new[] { domain })
                                           select e)
                             .ToArray(); // force execution
                        }
                    }
                }
            }
            return summary;
        }

        private ErrorSummary ValidateIdsExists<T>(EntityLayout layout, IList<T> entities)
        {
            ErrorSummary summary = null;
            var entitiesWithId = (from p in entities
                                  where (p as DomainObject).Id > 0
                                  select (p as DomainObject).Id);
            if (entitiesWithId.Count() > 0)
            {
                var idValues = entitiesWithId.Distinct().ToArray();
                var idsString = new StringBuilder();
                for (var idx = 0; idx < idValues.Length; idx++)
                {
                    idsString.Append(idValues[idx]);
                    if (idx < idValues.Length - 1)
                    {
                        idsString.Append(",");
                    }
                }
                var query = new SimpleQuery<object>(typeof (T),
                                                    string.Format(
                                                        "select distinct d.Id from {0} d where d.Id in ({1})",
                                                        typeof (T).Name, idsString));
                var result = query.Execute();
                if (result.Length != idValues.Length)
                {
                    var ids = result.Cast<object>().ToList();
                    var missings = idValues.Cast<object>().Except(ids);
                    foreach (var missing in missings)
                    {
                        var entitiesWithErrors = (from e in entities
                                                  where (e as DomainObject).Id.Equals(missing)
                                                  select e);
                        foreach (var e in entitiesWithErrors)
                        {
                            AddErrorToSummary(ref summary, entities.IndexOf(e), 1, layout.PrimaryKey, missing);
                        }
                    }
                }
            }
            return summary;
        }

        private void AddErrorToSummary(ref ErrorSummary summary, int row, int col, Field property, object val)
        {
            if (summary == null)
            {
                summary = new ErrorSummary();
                summary.Errors = new List<ErrorDescription>();
            }
            summary.Errors.Add(new ErrorDescription
                                   {
                                       Line = row,
                                       Column = col,
                                       Property = property,
                                       Severity = ErrorSeverity.Error,
                                       Type = ErrorType.Data,
                                       Description =
                                           string.Format(
                                           "Valor de la referencia [{0}] no existe [columna: {1}, renglon: {2}]",
                                           val, col, row)
                                   });
        }
    }
}